# Custom Extension Commands

Custom commands, Skill templates, sub-agent role management, and side-channel Q&A.

Back to: [Command Panel Guide](./09.0.Command%20Panel%20Guide.md)

---

## `/custom`

Create custom commands.

- **Function**: Open custom command configuration panel
- **Features**:
  - Create new custom commands
  - Supports two types:
    - **execute**: Execute command in terminal
    - **prompt**: Send prompt to AI
    - **panel**: Load AnyPanel plugin to display a panel
  - Supports global and project level
  - **Supports additional input**: You can add extra arguments after the command. If the template contains a `$ARGUMENTS` placeholder it is replaced in-place; otherwise arguments are appended to the command or prompt (see below)
- **Storage Location**:
  - Global: `~/.snow/commands/`
  - Project: `.snow/commands/`
- **Examples**:
  - Type `/custom` to open configuration interface
  - Using additional input: `/mycommand extra args` - arguments replace the `$ARGUMENTS` placeholder in the template, or are appended to the end

### Panel Type and AnyPanel Plugin

Custom commands of type `panel` load a plugin from the `~/.snow/plugin/anypanel/` directory and display a panel.

- **Plugin directory**: `~/.snow/plugin/anypanel/`
- **Supported file formats**: `.js` / `.mjs` / `.cjs`
- **command field**: The id of the AnyPanel plugin
- **Plugin interface**: The module must export `default`, `anyPanel`, or `anyPanels`, containing `id`, `name`, `init`, `handleInput`, `getStatus` methods, plus either `render` (recommended) or `getRenderLines`

#### Rendering Modes

AnyPanel plugins support two rendering modes — implement at least one:

1. **Rich-text mode** (recommended): Implement `render(state, ctx)`. AnyPanelScreen injects React, ink components (`Box`, `Text`, `Newline`, `Spacer`), the current theme (`theme`), user language, terminal width, and a `forceRerender()` function into `ctx`, letting the plugin build colored, bordered, laid-out interfaces.
2. **Plain-text mode** (backward compatible): Implement `getRenderLines(state)` returning an array of strings, rendered line-by-line by AnyPanelScreen with `<Text>`.

When both are present, `render()` takes precedence.

#### `render(state, ctx)` Render Context

The `ctx` parameter contains:

| Field           | Type      | Description                                                                                                          |
| --------------- | --------- | -------------------------------------------------------------------------------------------------------------------- |
| `React`         | namespace | React namespace for `React.createElement`                                                                            |
| `Box`           | Component | ink Box component                                                                                                    |
| `Text`          | Component | ink Text component                                                                                                   |
| `Newline`       | Component | ink Newline component                                                                                                |
| `Spacer`        | Component | ink Spacer component                                                                                                 |
| `theme`         | Theme     | Full theme color config (`theme.colors.xxx`)                                                                         |
| `language`      | string    | Current user language                                                                                                |
| `terminalWidth` | number    | Terminal width                                                                                                       |
| `sessionId`     | string    | Current session ID; empty string if no session context                                                               |
| `sessionJson`   | string    | Current session as raw JSON; empty string if no session context. `JSON.parse` to read messages, title, summary, etc. |
| `forceRerender` | function  | Request a re-render (use after async ops complete)                                                                   |

#### `init(ctx)` Init Context

The `ctx` parameter received by `init(ctx)` contains:

| Field            | Type   | Description                                                                                                          |
| ---------------- | ------ | -------------------------------------------------------------------------------------------------------------------- |
| `terminalWidth`  | number | Terminal width                                                                                                       |
| `terminalHeight` | number | Terminal height (estimated)                                                                                          |
| `language`       | string | Current user language                                                                                                |
| `cwd`            | string | Current working directory                                                                                            |
| `sessionId`      | string | Current session ID; empty string if no session context                                                               |
| `sessionJson`    | string | Current session as raw JSON; empty string if no session context. `JSON.parse` to read messages, title, summary, etc. |

> `sessionId` and `sessionJson` are available in both `init(ctx)` and `render(state, ctx)`. When the panel is opened without a session context (e.g. standalone panel), both fields are empty strings.

#### Plugin Interface Overview

```ts
interface AnyPanelPlugin<S = unknown> {
	id: string;
	name: string;
	description?: string | Partial<Record<Language, string>>;
	author?: string;
	version?: string;
	enable?: boolean;

	// Optional: auto-refresh interval in ms (min 100ms).
	// When set, the panel repaints on a timer without user input.
	// Useful for real-time monitoring panels.
	refreshIntervalMs?: number;

	init(ctx: AnyPanelInitContext): S;
	handleInput(state: S, input: AnyPanelInput): S;

	// Render method (pick one, render takes precedence)
	render?(state: S, ctx: AnyPanelRenderContext): ReactNode;
	getRenderLines?(state: S): string[];

	getStatus(state: S): 'active' | 'done';
	getHint?(state: S): string;

	// --- Lifecycle hooks (all optional) ---
	onMount?(state: S): void; // Called after init() when panel opens
	onUnmount?(state: S): void; // Called when panel closes / component unmounts
	onFocus?(state: S): void; // Called when panel gains focus
	onBlur?(state: S): void; // Called when panel loses focus
}
```

The generic type parameter `<S>` defaults to `unknown`. You can leave it untyped (the default works everywhere) or specify it for stronger type safety in TypeScript authoring environments. All `state` parameters use type `S`.

#### Rich-text Mode Example

```js
// ~/.snow/plugin/anypanel/my-panel.mjs
export default {
	id: 'my-panel',
	name: 'My Panel',
	init() {
		return {count: 0};
	},
	handleInput(state, input) {
		if (input.key.return) return {...state, count: state.count + 1};
		return state;
	},
	render(state, ctx) {
		const {React, Box, Text, theme} = ctx;
		const e = React.createElement;
		return e(
			Box,
			{flexDirection: 'column'},
			e(
				Text,
				{color: theme.colors.success, bold: true},
				`Count: ${state.count}`,
			),
		);
	},
	getStatus() {
		return 'active';
	},
	getHint() {
		return 'Enter: increment | ESC: exit';
	},
};
```

#### Plain-text Mode Example

```js
// ~/.snow/plugin/anypanel/my-panel.mjs
export default {
	id: 'my-panel',
	name: 'My Panel',
	init() {
		return {count: 0};
	},
	handleInput(state, input) {
		if (input.key.return) return {...state, count: state.count + 1};
		return state;
	},
	getRenderLines(state) {
		return [`Count: ${state.count}`];
	},
	getStatus() {
		return 'active';
	},
	getHint() {
		return 'Enter: increment | ESC: exit';
	},
};
```

#### `description` Multi-language Support

The `description` field accepts either a plain string (used for all languages) or a multi-language object. When using a multi-language object, the priority is: current language → `en` → first available language.

#### Timer-based Auto-refresh

By default, AnyPanel is event-driven (key presses trigger rendering; there is no tick loop). If you need to fetch network data in `init()`, use a synchronous approach (e.g. `execSync` calling `curl`). After async operations complete, call `ctx.forceRerender()` to trigger a repaint.

For real-time monitoring panels (e.g. process monitoring, log streams, system resources), set `refreshIntervalMs` to enable periodic auto-refresh. The panel will repaint on the timer without requiring user input:

```js
export default {
	id: 'monitor',
	name: 'Process Monitor',
	refreshIntervalMs: 2000, // repaint every 2 seconds
	init() {
		return {processes: []};
	},
	handleInput(state, input) {
		// ... handle user navigation
		return state;
	},
	render(state, ctx) {
		// IMPORTANT: fetch fresh data synchronously here on each render
		// refreshIntervalMs only triggers a repaint — it does NOT call handleInput
		// or update state. The plugin must read current data in render().
		const {execSync} = require('child_process');
		const out = execSync('ps aux', {encoding: 'utf8'});
		const processes = out.split('\n').slice(0, 20);
		// ... render process list
	},
	getStatus() {
		return 'active';
	},
};
```

- Minimum effective interval is 100ms (values below are clamped to 100ms).
- If not set, the panel remains purely event-driven (only key presses trigger re-render).
- **The timer only triggers a repaint — it does not call `handleInput()` or update `state`.** The plugin must fetch fresh data synchronously inside `render()` (e.g. via `execSync`).

#### Keyboard Input

The `input` parameter in `handleInput(state, input)` contains:

| Field   | Type   | Description                 |
| ------- | ------ | --------------------------- |
| `input` | string | Raw character typed         |
| `key`   | object | Key state flags (see below) |

The `key` object supports the following fields (all `boolean`):

| Key          | Description                          |
| ------------ | ------------------------------------ |
| `upArrow`    | Up arrow key                         |
| `downArrow`  | Down arrow key                       |
| `leftArrow`  | Left arrow key                       |
| `rightArrow` | Right arrow key                      |
| `return`     | Enter / Return key                   |
| `escape`     | Escape key (always closes the panel) |
| `backspace`  | Backspace key                        |
| `delete`     | Delete key                           |
| `tab`        | Tab key                              |
| `pageUp`     | Page Up key                          |
| `pageDown`   | Page Down key                        |
| `home`       | Home key                             |
| `end`        | End key                              |
| `ctrl`       | Ctrl modifier                        |
| `shift`      | Shift modifier                       |
| `meta`       | Meta / Alt modifier                  |

#### Lifecycle Hooks

AnyPanel plugins can implement optional lifecycle hooks for resource management:

| Hook        | When called                                                                                                                                                                                                                           |
| ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `onMount`   | After `init()`, when the panel opens. Suitable for starting async tasks, opening connections.                                                                                                                                         |
| `onUnmount` | When the panel closes (ESC / `getStatus()` returns `'done'` / component unmounts). Suitable for cleaning up timers, closing connections. Called exactly once — if already invoked via the close flow, the unmount cleanup is skipped. |
| `onFocus`   | When the panel gains focus. Currently called once on mount (after `onMount`).                                                                                                                                                         |
| `onBlur`    | When the panel loses focus. Currently called once before closing (before `onUnmount`).                                                                                                                                                |

All hooks are optional. If a hook throws an error, it is silently caught and does not affect panel operation.

#### Render Error Recovery

If `render()` or `getRenderLines()` throws an error, AnyPanelScreen keeps the last successfully rendered screen and shows the error message in a warning color below the panel content. The panel stays open — the user can continue interacting, and the error is cleared automatically once the next render succeeds.

**Example command file** (`~/.snow/commands/mypanel.json`):

```json
{
	"type": "panel",
	"command": "my-plugin-id",
	"description": "My custom panel"
}
```

### `description` (optional)

Custom command JSON supports an optional `description` field. It is shown in the command panel suggestions (the list you see after typing `/`) so you can keep prompts readable.

- **Compatibility**: If `description` is missing/empty, Snow CLI falls back to showing `command` (for `type: "prompt"` commands, that is the full prompt), so existing command files keep working.
- **How to set**: You can enter it when creating a command via `/custom`; leave it empty to skip.

**Example:**

```json
{
	"type": "prompt",
	"command": "Summarize the current conversation",
	"description": "Summarize this chat"
}
```

### `$ARGUMENTS` Placeholder

By default, arguments typed after a command are **appended to the end of the command or prompt**. To insert arguments at a specific position in the template, use the `$ARGUMENTS` placeholder in the `command` field:

- **Template contains `$ARGUMENTS`**: arguments replace the placeholder in-place (all occurrences are replaced), letting the command author control exactly where user input lands
- **Template has no `$ARGUMENTS`**: arguments are appended to the end (legacy behavior preserved; existing commands keep working)
- **Empty arguments**: the placeholder is replaced with an empty string, so `$ARGUMENTS` never leaks into the prompt or terminal command

This placeholder convention matches Snow CLI's Skills path and Claude Code's slash commands, easing cross-tool migration. Both `execute` and `prompt` types support it.

**Example command file** (`~/.snow/commands/fix-issue.json`):

```json
{
	"type": "prompt",
	"command": "Fix GitHub issue $ARGUMENTS following our coding standards.",
	"description": "Fix GitHub issue per our standards"
}
```

After running `/fix-issue 123`, the content sent to the AI is:

```
Fix GitHub issue 123 following our coding standards.
```

If the template has no placeholder (e.g. the "Summarize this chat" example above), running `/summary extra note` appends the arguments to the end: `Summarize the current conversation extra note`.

### Namespaced custom commands

Custom commands support a namespaced format: `/<namespace>:<command> [args...]`.

This is useful when you want to organize many commands by feature/team/environment.

**Directory mapping (command name is inferred from file path):**

- `.snow/commands/build.json` -> `/build`
- `.snow/commands/deploy/stage.json` -> `/deploy:stage`
- `.snow/commands/deploy/prod.json` -> `/deploy:prod`

The same rule applies to the global directory `~/.snow/commands/`.

**Notes / constraints:**

- Arguments are separated by whitespace: `/deploy:stage --dry-run`
- `:` is reserved as the namespace separator.
- Namespace uses folder segments separated by `/`.
- Namespace segments cannot be `.` or `..`, and cannot contain `:` or `\\`.
- The command part cannot contain whitespace, `\\`, `/`, or `:` (and cannot be `.` or `..`).

## `/skills`

Create skill templates.

- **Function**: Open skill creation dialog
- **Features**:
  - Generate SKILL.md (main document)
  - Generate reference.md (detailed reference)
  - Generate examples.md (usage examples)
  - Create templates/ (template files)
  - Create scripts/ (auxiliary scripts)
- **Storage Location**:
  - Global: `~/.snow/skills/`
  - Project: `.snow/skills/`
- **Naming Rules**: Lowercase letters, numbers, and hyphens; use `/` to namespace (max 64 chars per segment)
- **Directory mapping**: `~/.snow/skills/<namespace>/<skill>/SKILL.md` -> skill id `<namespace>/<skill>`
- **Example**: Type `/skills`, then enter `team/my-skill` in the dialog

## Deleting Custom Commands/Skills

After creating custom commands, use `/<command-name> -d` to delete:

- **Delete custom command**: `/mycommand -d`
- **Location Recognition**: Automatically recognizes global or project level
- **Example**: If created `/deploy` command, use `/deploy -d` to delete
- **Namespaced example**: If created `/deploy:stage` command, use `/deploy:stage -d` to delete

## `/role-subagent`

Sub-agent role definition file management.

- **Function**: Manage ROLE files for sub-agents (`ROLE-<agentName>.md`), defining independent role behavior for each sub-agent
- **Features**:
  - **Create**: `/role-subagent` - Open an interactive creation panel, select scope then sub-agent
  - **Delete**: `/role-subagent -d` or `/role-subagent --delete` - Open the deletion panel to select a sub-agent role file to delete
  - **List**: `/role-subagent -l` or `/role-subagent --list` - Open the sub-agent role management panel to view and manage existing role files
- **Storage Location**:
  - Global: `~/.snow/ROLE-<agentName>.md`
  - Project: `<project-root>/ROLE-<agentName>.md`
- **Priority**: When loading custom roles, project-level takes precedence over global-level
- **Panel Operations**:
  - **Creation Panel**:
    1. Select location: `G` - Global, `P` - Project, `ESC` - Cancel
    2. Select sub-agent: `↑/↓` - Navigate, `Enter` - Select, `ESC` - Go back
    3. Confirm: `Y` - Confirm creation, `N` - Go back
  - **Deletion Panel**:
    1. Select location: `G` - Global, `P` - Project, `ESC` - Cancel
    2. Select file: `↑/↓` - Navigate, `Enter` - Select, `ESC` - Go back
    3. Confirm: `Y` - Confirm deletion, `N` - Go back
  - **List Panel**:
    - `Tab` - Switch Global / Project
    - `↑/↓` - Move selection
    - `D` - Delete selected role file (requires confirmation: `Y` confirm, `N/ESC` cancel)
    - `ESC` - Close panel
- **Use Cases**: When you need to customize role behavior for specific sub-agents (e.g., explore agent, plan agent, etc.)
- **Examples**:
  - `/role-subagent` - Open creation panel
  - `/role-subagent -d` - Open deletion panel
  - `/role-subagent -l` - Open list management panel

## `/btw`

Quick question (side-channel Q&A).

- **Function**: Ask a standalone quick question to the AI without affecting the current conversation context
- **Features**:
  - Streams the AI response in a side panel
  - Response content is not written to the main conversation history
  - Supports scrolling through the response
- **Panel Operations**:
  - **Streaming phase**: `ESC` - Abort and close
  - **Done phase**: `↑/↓` - Scroll through response, `Enter` - Close, `ESC` - Close
  - **Error phase**: `Enter` - Close, `ESC` - Close
- **Use Cases**: Need to quickly ask a question unrelated to the current task without interrupting the conversation context
- **Example**: `/btw explain generics in TypeScript`
